Reading OPUS files

source

This code is modified from the original OPUSreader2 documentation vignettes/opusreader2_introduction.Rmd by Philip Baumann and Thomas Knecht. It reads OPUS binary files and extracts metadata and absorbance data, which can then be plotted.

Plots & Metadata

The main function, read_opus(), reads one or more OPUS files and returns a nested list of class list_opusreader2. Each list contains both the spectral data and metadata for each file. The dsn argument is the data source name. It can be a character vector of folder paths (to read files recursively) or specific OPUS file paths. Start by testing the read_opus function on your corrected spectra folder, and saving the output of the first file.

#load opusreader2 from github.com/spectral-cockpit
library(opusreader2)

#save the path to your corrected spectra
# # Mac path
# corr_spectra <- "/Users/adrianneseiden/Library/CloudStorage/Box-Box/Salk Institute Project/AKS Salk files/Adrianne_FTIRdata/Corrected_files" # nolint
#  # Windows path
# corr_spectra <- "C:/Users/adria/Box/Box-Box/Salk Institute Project/AKS Salk files/Adrianne_FTIRdata/Corrected_files" # nolint
# # Relative path
# corr_spectra <- "../Adrianne_FTIRdata/Corrected_files"

#save the data from your corrected files as 'data_test'
data_test <- read_opus(dsn = corr_spectra)
#check the names of the list
names(data_test)
>  [1] "DRIFTS_pot001_13Cwheat_wk0_root_250725_corr.0"        
>  [2] "DRIFTS_pot030_13Csoy_wk10_root_250725_corr.0"         
>  [3] "DRIFTS_pot032_13Cwheat_wk0_root_250725_corr.0"        
>  [4] "DRIFTS_pot047_13Crice_wk0_root_250725_corr.0"         
>  [5] "DRIFTS_pot053_13Cwheat_wk10_root_250725_corr.0"       
>  [6] "DRIFTS_pot055_13Crice_wk10_root_250725_corr.0"        
>  [7] "DRIFTS_pot055qmark_13Crice_wk10_root_250725_corr.0"   
>  [8] "DRIFTS_pot062_13Csoy_wk0_root_250725_corr.0"          
>  [9] "DRIFTS_pot079_13Crice_wk0_root_250725_corr.0"         
> [10] "DRIFTS_pot080_13Csoy_wk10_root_250725_corr.0"         
> [11] "DRIFTS_pot094_13Csoy_wk10_root_250725_corr.0"         
> [12] "DRIFTS_pot094_13Csoy_wk10_soil_250630_corr.0"         
> [13] "DRIFTS_pot103_13Crice_wk0_root_recNMR_250725_corr.0"  
> [14] "DRIFTS_pot103_13Crice_wk0_root_vial1_250725_corr.0"   
> [15] "DRIFTS_pot103_13Crice_wk0_root_vial2_250725_corr.0"   
> [16] "DRIFTS_pot103_13Crice_wk0_root_vial3_250725_corr.0"   
> [17] "DRIFTS_pot107_12Crice_wk0_root_really13_250725_corr.0"
# define 'meas_1' as the first element of the 'data_test' list
meas_1 <- data_test[[1]]

Next I defined a function, plotSpectrum, to plot the spectral data from a single sample (meas_1)

# data is a list containing OPUS file data, including absorbance and metadata.

plotSpectrum <- function(data) { # nolint: object_name_linter.
  ab_data <- data$ab
  if (!is.null(ab_data) &&
        !is.null(ab_data$wavenumbers) && !is.null(ab_data$data) &&
        is.numeric(ab_data$wavenumbers) && is.numeric(ab_data$data) &&
        length(ab_data$wavenumbers) == length(ab_data$data) &&
        all(is.finite(ab_data$wavenumbers)) && all(is.finite(ab_data$data))) {
    plot(
      ab_data$wavenumbers, ab_data$data, type = "l",
      xlab = "Wavenumber (cm⁻¹)", ylab = "Absorbance",
      main = data$basic_metadata$dsn_filename,
      xlim = rev(range(ab_data$wavenumbers))
    )
  } else {
    cat("Absorbance data not found or not valid in this file.\n")
    str(ab_data)
  }
}
plotSpectrum(meas_1)

I then defined the metadataTable function, to extract and display the a subset of the metadata stored in the data extracted by ‘read_opus’ in a table format.

To view all the available data categories, run ‘names(meas_1)’ in the console. To view the parameters within these categories use str(meas_1$category_name) where ‘category_name’ is ‘basic_metadata’, ‘optics’, etc.

The plotMetadata function combines the plot and table for a single spectrum.

plotMetadata <- function(data) { # nolint: object_name_linter.
  plotSpectrum(data)
  metadataTable(data)
}
plotMetadata(meas_1)

Metadata
Parameter Value
File Name DRIFTS_pot001_13Cwheat_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:18:39 GMT-7
Max Y 0.799607157707214
Min Y -0.0012896629050374
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56884765625
Experiment (method) SALK_soils_method.xpm

Combined plot and table (folder)

The following for loop runs plotMetadata on each spectrum in the data_test list, which contains all the spectra in the folder. It will plot each spectrum and print its metadata below.

for (i in seq_along(data_test)) {
  cat("#### Spectrum", i, ":", names(data_test)[i], "\n\n")
  data_i <- data_test[[i]]
  ab_data <- data_i$ab
  valid <- !is.null(ab_data) &&
    !is.null(ab_data$wavenumbers) && !is.null(ab_data$data) &&
    is.numeric(ab_data$wavenumbers) && is.numeric(ab_data$data) &&
    length(ab_data$wavenumbers) == length(ab_data$data) &&
    all(is.finite(ab_data$wavenumbers)) && all(is.finite(ab_data$data))
  if (valid) {
    plotSpectrum(data_i)
    print(metadataTable(data_i))
  } else {
    cat("Data not valid or missing for this spectrum.\n")
    str(ab_data)
  }
}

Spectrum 1 : DRIFTS_pot001_13Cwheat_wk0_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot001_13Cwheat_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:18:39 GMT-7
Max Y 0.799607157707214
Min Y -0.0012896629050374
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56884765625
Experiment (method) SALK_soils_method.xpm

Spectrum 2 : DRIFTS_pot030_13Csoy_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot030_13Csoy_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:21:37 GMT-7
Max Y 0.84355878829956
Min Y -0.00326710939407349
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56689453125
Experiment (method) SALK_soils_method.xpm

Spectrum 3 : DRIFTS_pot032_13Cwheat_wk0_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot032_13Cwheat_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:22:45 GMT-7
Max Y 0.909599244594574
Min Y -0.00415003532543778
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56982421875
Experiment (method) SALK_soils_method.xpm

Spectrum 4 : DRIFTS_pot047_13Crice_wk0_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot047_13Crice_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:23:56 GMT-7
Max Y 0.712926685810089
Min Y -0.00133463030215353
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Spectrum 5 : DRIFTS_pot053_13Cwheat_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot053_13Cwheat_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:24:39 GMT-7
Max Y 0.997280716896057
Min Y -0.00962971057742834
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Spectrum 6 : DRIFTS_pot055_13Crice_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot055_13Crice_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:26:35 GMT-7
Max Y 0.816974341869354
Min Y -0.00123318459372967
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56689453125
Experiment (method) SALK_soils_method.xpm

Spectrum 7 : DRIFTS_pot055qmark_13Crice_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot055qmark_13Crice_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:25:51 GMT-7
Max Y 0.907369911670685
Min Y -0.00234174402430654
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Spectrum 8 : DRIFTS_pot062_13Csoy_wk0_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot062_13Csoy_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:27:28 GMT-7
Max Y 0.687717080116272
Min Y -0.00538174575194716
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56884765625
Experiment (method) SALK_soils_method.xpm

Spectrum 9 : DRIFTS_pot079_13Crice_wk0_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot079_13Crice_wk0_root_250725_corr.0
Timestamp 2025-07-25 20:28:00 GMT-7
Max Y 0.816869914531708
Min Y -0.00257659493945539
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56787109375
Experiment (method) SALK_soils_method.xpm

Spectrum 10 : DRIFTS_pot080_13Csoy_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot080_13Csoy_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:28:56 GMT-7
Max Y 0.77625048160553
Min Y -0.00814440380781889
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56689453125
Experiment (method) SALK_soils_method.xpm

Spectrum 11 : DRIFTS_pot094_13Csoy_wk10_root_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot094_13Csoy_wk10_root_250725_corr.0
Timestamp 2025-07-25 20:29:09 GMT-7
Max Y 1.27848649024963
Min Y -0.00875786785036325
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56689453125
Experiment (method) SALK_soils_method.xpm

Spectrum 12 : DRIFTS_pot094_13Csoy_wk10_soil_250630_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot094_13Csoy_wk10_soil_250630_corr.0
Timestamp 2025-07-25 20:46:03 GMT-7
Max Y 0.821687042713165
Min Y -0.00153941253665835
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.454833984375
Experiment (method) SALK_soils_method.xpm

Spectrum 13 : DRIFTS_pot103_13Crice_wk0_root_recNMR_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot103_13Crice_wk0_root_recNMR_250725_corr.0
Timestamp 2025-07-25 20:30:32 GMT-7
Max Y 0.836920917034149
Min Y -0.00138377456460148
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56982421875
Experiment (method) SALK_soils_method.xpm

Spectrum 14 : DRIFTS_pot103_13Crice_wk0_root_vial1_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot103_13Crice_wk0_root_vial1_250725_corr.0
Timestamp 2025-07-25 20:30:55 GMT-7
Max Y 0.826825261116028
Min Y -0.000596170837525278
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56787109375
Experiment (method) SALK_soils_method.xpm

Spectrum 15 : DRIFTS_pot103_13Crice_wk0_root_vial2_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot103_13Crice_wk0_root_vial2_250725_corr.0
Timestamp 2025-07-25 20:31:26 GMT-7
Max Y 0.984898567199707
Min Y -0.00565983727574348
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Spectrum 16 : DRIFTS_pot103_13Crice_wk0_root_vial3_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot103_13Crice_wk0_root_vial3_250725_corr.0
Timestamp 2025-07-25 20:31:46 GMT-7
Max Y 0.983739078044891
Min Y -0.0026757272426039
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Spectrum 17 : DRIFTS_pot107_12Crice_wk0_root_really13_250725_corr.0

Metadata
Parameter Value
File Name DRIFTS_pot107_12Crice_wk0_root_really13_250725_corr.0
Timestamp 2025-07-25 20:32:00 GMT-7
Max Y 1.03206670284271
Min Y -0.00172662467230111
Aperture Setting 6 mm
Scanner Velocity 10.0S
Result Spectrum AB
Resolution 4
Sample Scans 400
End Frequency 400
Start Frequency 4000
Duration 364.56591796875
Experiment (method) SALK_soils_method.xpm

Stacking multiple spectra

Stacking re-runs

This code looks for files with the same sample number and stacks their spectra, to check whether reruns are consistent.

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  echo = TRUE,
  results = "markup",
  include = TRUE
)
library(opusreader2)
library(RColorBrewer)

#save the path to your corrected spectra
# # Mac path
# corr_spectra <- "/Users/adrianneseiden/Library/CloudStorage/Box-Box/Salk Institute Project/AKS Salk files/Adrianne_FTIRdata/Corrected_files" # nolint
#  # Windows path
# corr_spectra <- "C:/Users/adria/Box/Box-Box/Salk Institute Project/AKS Salk files/Adrianne_FTIRdata/Corrected_files" # nolint
# Relative path
# corr_spectra <- "../Adrianne_FTIRdata/Corrected_files"

data_test <- read_opus(dsn = corr_spectra)

# Helper to extract sample number from filename (e.g., "pot094")
extract_sample_number <- function(filename) {
  m <- regexpr("[a-z]{3}[0-9]+", filename)
  if (m[1] != -1) {
    regmatches(filename, m)
  } else {
    NA
  }
}

# Helper to extract sample type (e.g.,"soil", "root")
extract_sample_type <- function(filename) {
  m <- regexpr("soil|root", filename, ignore.case = TRUE)
  if (m[1] != -1) {
    tolower(regmatches(filename, m))
  } else {
    NA
  }
}

# Group files by sample number & sample type
sample_numbers <- sapply(names(data_test), extract_sample_number)
sample_types <- sapply(names(data_test), extract_sample_type)

# Create unique combinations of sample number and sample type
sample_combinations <- paste(sample_numbers, sample_types, sep = "_")
unique_combinations <- unique(sample_combinations[!is.na(sample_numbers)
                                                  & !is.na(sample_types)])

# Plot stacked spectra for each unique combination
library(RColorBrewer)
for (combination in unique_combinations) {
  idx <- which(sample_combinations == combination)
  if (length(idx) > 1) {
    # Extract sample number and type from combination
    parts <- strsplit(combination, "_")[[1]]
    sample_num <- parts[1]
    sample_type <- parts[2]

    spectra <- data_test[idx]
    colors <- brewer.pal(min(length(spectra), 8), "Set1")
    plot(NULL, xlim = rev(range(spectra[[1]]$ab$wavenumbers)),
         ylim = range(sapply(spectra, function(x) x$ab$data), na.rm = TRUE),
         xlab = "Wavenumber (cm⁻¹)", ylab = "Absorbance",
         main = paste("Stacked spectra for", sample_num, "-", sample_type),
         bty = "l")
    for (i in seq_along(spectra)) {
      ab_data <- spectra[[i]]$ab
      if (!is.null(ab_data) && !is.null(ab_data$wavenumbers) &&
            !is.null(ab_data$data)) {
        lines(ab_data$wavenumbers, ab_data$data, col = colors[i], lwd = 2)
      }
    }
    legend("topright",
           inset = c(-0.05, -0.02),
           legend = names(spectra),
           col = colors[seq_along(spectra)],
           lwd = 2,
           xpd = TRUE,
           bty = "n")
  }
}

Stacking by crop & time

This code stacks spectra of the same crop and timepoint.

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  echo = TRUE,
  results = "markup",
  include = TRUE
)
# Helper functions to extract crop and timepoint from filename
extract_crop <- function(filename) {
  m <- regexpr("rice|wheat|soy", filename, ignore.case = TRUE)
  if (m[1] != -1) tolower(regmatches(filename, m)) else NA
}
extract_time <- function(filename) {
  m <- regexpr("wk[0-9]+", filename, ignore.case = TRUE)
  if (m[1] != -1) tolower(regmatches(filename, m)) else NA
}

# Get crop and time for each file
crops <- sapply(names(data_test), extract_crop)
times <- sapply(names(data_test), extract_time)

# Unique crop-time combinations
combos <- na.omit(unique(paste(crops, times, sep = "_")))

library(RColorBrewer)
for (combo in combos) {
  idx <- which(paste(crops, times, sep = "_") == combo)
  if (length(idx) > 1) {
    spectra <- data_test[idx]
    colors <- brewer.pal(min(length(spectra), 8), "Set1")
    plot(NULL, xlim = rev(range(spectra[[1]]$ab$wavenumbers)),
         ylim = range(sapply(spectra, function(x) x$ab$data), na.rm = TRUE),
         xlab = "Wavenumber (cm⁻¹)", ylab = "Absorbance",
         main = paste("Stacked spectra for", combo),
         bty = "l")
    for (i in seq_along(spectra)) {
      ab_data <- spectra[[i]]$ab
      if (!is.null(ab_data) &&
            !is.null(ab_data$wavenumbers)
          && !is.null(ab_data$data)) {
        lines(ab_data$wavenumbers, ab_data$data, col = colors[i], lwd = 2)
      }
    }
    legend("topright",
           inset = c(-0.05, -0.02),
           legend = names(spectra),
           col = colors[seq_along(spectra)],
           lwd = 2,
           xpd = TRUE,
           bty = "n")
  }
}

Reading DPT files

This code is modified from code by Stephany Soledad Chacon, “Box> Salk Institute Project> Salk Data> FTIR_Intact_decomposition_pots> FTIR_analysis.Rmd”. Using data in the DPT format, we can extrapolate sample attributes from the file names, then normalize and visualize spectra across replicates, crops, and timepoints. The width of the line bounding the spectra represents the range of the data.

Importing FTIR data using readr

Define file path and import multiple files into one data frame

#> Files found in directory:
#>  [1] "DRIFTS_KBr250630_400scans1.0.dpt"                    
#>  [2] "DRIFTS_pot001_13Cwheat_wk0_root_250725.0.dpt"        
#>  [3] "DRIFTS_pot030_13Csoy_wk10_root_250725.0.dpt"         
#>  [4] "DRIFTS_pot032_13Cwheat_wk0_root_250725.0.dpt"        
#>  [5] "DRIFTS_pot047_13Crice_wk0_root_250725.0.dpt"         
#>  [6] "DRIFTS_pot053_13Cwheat_wk10_root_250725.0.dpt"       
#>  [7] "DRIFTS_pot055_13Crice_wk10_root_250725.0.dpt"        
#>  [8] "DRIFTS_pot055_13Crice_wk10_root_qmark_250725.0.dpt"  
#>  [9] "DRIFTS_pot062_13Csoy_wk0_root_250725.0.dpt"          
#> [10] "DRIFTS_pot079_13Crice_wk0_root_250725.0.dpt"         
#> [11] "DRIFTS_pot080_13Csoy_wk10_root_250725.0.dpt"         
#> [12] "DRIFTS_pot094_13Csoy_wk10_root_250725.0.dpt"         
#> [13] "DRIFTS_pot094_13Csoy_wk10_soil_250630.0.dpt"         
#> [14] "DRIFTS_pot103_13Crice_wk0_root_recNMR_250725.0.dpt"  
#> [15] "DRIFTS_pot103_13Crice_wk0_root_vial1_250725.0.dpt"   
#> [16] "DRIFTS_pot103_13Crice_wk0_root_vial2_250725.2.dpt"   
#> [17] "DRIFTS_pot103_13Crice_wk0_root_vial3_250725.0.dpt"   
#> [18] "DRIFTS_pot107_12Crice_wk0_root_really13_250725.0.dpt"
#> Number of .dpt files: 18

Extract sample info from filename

Using the long format dataframe dpt_data format example: DRIFTS_pot094_13Csoy_wk10_soil_250630.0.dpt

#> Column filenames: wavenumber absorbance filename
#> [1] "pot"
#>  [1] "001" "030" "032" "047" "053" "055" "062" "079" "080" "094" "103" "107"
#> [1] 13 12
#> [1] "wheat" "soy"   "rice"
#> [1] "wk0"  "wk10"
#> [1] "root" "soil"
#> [1] "250725" "250630"
#> [1] NA         "qmark"    "recNMR"   "vial1"    "vial2"    "vial3"    "really13"
#> # A tibble: 6 × 11
#>   wavenumber absorbance filename      source ID    isotope crop  timepoint type 
#>        <dbl>      <dbl> <chr>         <chr>  <chr>   <dbl> <chr> <chr>     <chr>
#> 1      3998.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> 2      3997.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> 3      3995.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> 4      3994.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> 5      3993.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> 6      3991.     0.0115 DRIFTS_pot00… pot    001        13 wheat wk0       root 
#> # ℹ 2 more variables: run_date <chr>, notes <chr>

Plotting by timepoint and type

Week 0

FALSE [1] "001" "032" "062" "079" "103" "107"

Week 10

#> [1] "030" "053" "055" "080" "094"

#> [1] "094"

Plotting by crop

wheat roots

#> [1] "001" "032" "053"

rice roots

#> [1] "055" "079" "103" "107"

soy roots

#> [1] "030" "062" "080" "094"